/*
 * dlfcn-win32
 * Copyright (c) 2007 Ramiro Polla
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "dlfcn-win32.h"

/* Note:
 * MSDN says these functions are not thread-safe. We make no efforts to have
 * any kind of thread safety.
 */

typedef struct global_object {
  HMODULE hModule;
  struct global_object *previous;
  struct global_object *next;
} global_object;

static global_object first_object;

/* These functions implement a double linked list for the global objects. */
static global_object *global_search(HMODULE hModule)
{
  global_object *pobject;

  if (hModule == NULL) {
    return NULL;
  }

  for (pobject = &first_object; pobject ; pobject = pobject->next)
    if (pobject->hModule == hModule) {
      return pobject;
    }

  return NULL;
}

static void global_add(HMODULE hModule)
{
  global_object *pobject;
  global_object *nobject;

  if (hModule == NULL) {
    return;
  }

  pobject = global_search(hModule);

  /* Do not add object again if it's already on the list */
  if (pobject) {
    return;
  }

  for (pobject = &first_object; pobject->next ; pobject = pobject->next);

  nobject = static_cast<global_object *>(malloc(sizeof(global_object)));

  /* Should this be enough to fail global_add, and therefore also fail
   * dlopen?
   */
  if (!nobject) {
    return;
  }

  pobject->next = nobject;
  nobject->next = NULL;
  nobject->previous = pobject;
  nobject->hModule = hModule;
}

static void global_rem(HMODULE hModule)
{
  global_object *pobject;

  if (hModule == NULL) {
    return;
  }

  pobject = global_search(hModule);

  if (!pobject) {
    return;
  }

  if (pobject->next) {
    pobject->next->previous = pobject->previous;
  }
  if (pobject->previous) {
    pobject->previous->next = pobject->next;
  }

  free(pobject);
}

/* POSIX says dlerror( ) doesn't have to be thread-safe, so we use one
 * static buffer.
 * MSDN says the buffer cannot be larger than 64K bytes, so we set it to
 * the limit.
 */
static char error_buffer[65535];
static char *current_error;

static int copy_string(char *dest, int dest_size, const char *src)
{
  int i = 0;

  /* gcc should optimize this out */
  if (!src && !dest) {
    return 0;
  }

  for (i = 0 ; i < dest_size - 1 ; i++)
  {
    if (!src[i]) {
      break;
    }
    else {
      dest[i] = src[i];
    }
  }
  dest[i] = '\0';

  return i;
}

static void save_err_str(const char *str)
{
  DWORD dwMessageId;
  DWORD pos;

  dwMessageId = GetLastError();

  if (dwMessageId == 0) {
    return;
  }

  /* Format error message to:
   * "<argument to function that failed>": <Windows localized error message>
   */

  pos  = copy_string(error_buffer,     sizeof(error_buffer),     "\"");
  pos += copy_string(error_buffer + pos, sizeof(error_buffer) - pos, str);
  pos += copy_string(error_buffer + pos, sizeof(error_buffer) - pos, "\": ");

  size_t size = sizeof(error_buffer) - pos;
  wchar_t *wErrorBuffer = new wchar_t[size];
  size_t message_size = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwMessageId,
                                      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                                      wErrorBuffer, size, NULL);
  wcstombs(error_buffer + pos, wErrorBuffer, size);
  delete [] wErrorBuffer;
  pos += message_size;

  if (pos > 1)
  {
    /* POSIX says the string must not have trailing <newline> */
    if (error_buffer[pos - 2] == '\r' && error_buffer[pos - 1] == '\n') {
      error_buffer[pos - 2] = '\0';
    }
  }

  current_error = error_buffer;
}

static void save_err_ptr_str(const void *ptr)
{
  char ptr_buf[19]; /* 0x<pointer> up to 64 bits. */

  sprintf(ptr_buf, "0x%p", ptr);

  save_err_str(ptr_buf);
}

void *dlopen(const char *file, int mode)
{
  HMODULE hModule;
  UINT uMode;

  current_error = NULL;

  /* Do not let Windows display the critical-error-handler message box */
  uMode = SetErrorMode(SEM_FAILCRITICALERRORS);

  if (file == 0)
  {
    /* POSIX says that if the value of file is 0, a handle on a global
     * symbol object must be provided. That object must be able to access
     * all symbols from the original program file, and any objects loaded
     * with the RTLD_GLOBAL flag.
     * The return value from GetModuleHandle( ) allows us to retrieve
     * symbols only from the original program file. For objects loaded with
     * the RTLD_GLOBAL flag, we create our own list later on.
     */
    hModule = GetModuleHandle(NULL);

    if (!hModule) {
      save_err_ptr_str(file);
    }
  }
  else
  {
    char lpFileName[MAX_PATH];
    int i;

    /* MSDN says backslashes *must* be used instead of forward slashes. */
    for (i = 0 ; i < sizeof(lpFileName) - 1 ; i++)
    {
      if (!file[i]) {
        break;
      }
      else if (file[i] == '/') {
        lpFileName[i] = '\\';
      }
      else {
        lpFileName[i] = file[i];
      }
    }
    lpFileName[i] = '\0';

    /* POSIX says the search path is implementation-defined.
     * LOAD_WITH_ALTERED_SEARCH_PATH is used to make it behave more closely
     * to UNIX's search paths (start with system folders instead of current
     * folder).
     */
    size_t newsize = strlen(lpFileName) + 1;
    wchar_t *wFileName = new wchar_t[newsize];
    mbstowcs(wFileName, lpFileName, newsize);
    hModule = LoadLibraryEx(wFileName, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
    delete [] wFileName;

    /* If the object was loaded with RTLD_GLOBAL, add it to list of global
     * objects, so that its symbols may be retrieved even if the handle for
     * the original program file is passed. POSIX says that if the same
     * file is specified in multiple invocations, and any of them are
     * RTLD_GLOBAL, even if any further invocations use RTLD_LOCAL, the
     * symbols will remain global.
     */
    if (!hModule) {
      save_err_str(lpFileName);
    }
    else if ((mode & RTLD_GLOBAL)) {
      global_add(hModule);
    }
  }

  /* Return to previous state of the error-mode bit flags. */
  SetErrorMode(uMode);

  return (void *) hModule;
}

int dlclose(void *handle)
{
  HMODULE hModule = (HMODULE) handle;
  BOOL ret;

  current_error = NULL;

  ret = FreeLibrary(hModule);

  /* If the object was loaded with RTLD_GLOBAL, remove it from list of global
   * objects.
   */
  if (ret) {
    global_rem(hModule);
  }
  else {
    save_err_ptr_str(handle);
  }

  /* dlclose's return value in inverted in relation to FreeLibrary's. */
  ret = !ret;

  return (int) ret;
}

void *dlsym(void *handle, const char *name)
{
  FARPROC symbol;

  current_error = NULL;

  symbol = GetProcAddress(static_cast<HMODULE>(handle), name);

  if (symbol == NULL)
  {
    HMODULE hModule;

    /* If the handle for the original program file is passed, also search
     * in all globally loaded objects.
     */

    hModule = GetModuleHandle(NULL);

    if (hModule == handle)
    {
      global_object *pobject;

      for (pobject = &first_object; pobject ; pobject = pobject->next)
      {
        if (pobject->hModule)
        {
          symbol = GetProcAddress(pobject->hModule, name);
          if (symbol != NULL) {
            break;
          }
        }
      }
    }

    CloseHandle(hModule);
  }

  if (symbol == NULL) {
    save_err_str(name);
  }

  return (void *) symbol;
}

char *dlerror(void)
{
  char *error_pointer = current_error;

  /* POSIX says that invoking dlerror( ) a second time, immediately following
   * a prior invocation, shall result in NULL being returned.
   */
  current_error = NULL;

  return error_pointer;
}
